import { has, isNil } from 'lodash';
import { parseToRgb, rgbToColorString } from 'polished';

import { centsAmountToFloat, floatAmountToCents } from './currency-utils';
import { parseToBoolean } from './utils';

type FieldsConfig = {
  [key: string]:
    | {
        type: 'amount' | 'boolean' | 'color' | 'integer' | 'interval' | 'json' | 'stringArray' | 'float';
        modifier?: (value: any) => any;
      }
    | {
        type: 'alias';
        on: string;
        modifier?: (value: any) => any;
      };
};

/**
 * A tool to work with URL query parameters as generated by Next Router.
 */
export default class UrlQueryHelper {
  private config: FieldsConfig;

  constructor(config: FieldsConfig) {
    this.config = config;
  }

  static Decoders = {
    /**
     * @param {string} str
     * @returns {number}
     */
    amount: str => {
      if (isNil(str) || !str.length) {
        return null;
      } else {
        const result = floatAmountToCents(parseFloat(str));
        return isNaN(result) ? null : result;
      }
    },
    /**
     * @param {string} str
     * @returns {boolean}
     */
    boolean: str => {
      return isNil(str) || !str.length ? null : parseToBoolean(str, null);
    },
    /**
     * @param {string} str
     * @returns {string}
     */
    color: str => {
      if (!str) {
        return null;
      } else if (str.match(/^(?=\d*$)(?:.{3}|.{6}|.{8})$/)) {
        return `#${str}`; // Allow hex colors to be passed without the #
      } else {
        try {
          return rgbToColorString(parseToRgb(str));
        } catch {
          // Ignore errors, will return null
        }
      }

      return null;
    },
    /**
     * @param {string} str
     * @returns {number}
     */
    integer: str => {
      if (isNil(str) || !str.length) {
        return null;
      } else {
        const result = parseInt(str);
        return isNaN(result) || !Number.isSafeInteger(result) ? null : result;
      }
    },
    /**
     * @param {string} str
     * @returns {('month'|'year')}
     */
    interval: str => {
      if (!str) {
        return null;
      }

      const cleanStr = str.trim().replace(/ly$/, ''); // support for "monthly"/"yearly"
      return ['month', 'year', 'oneTime'].includes(cleanStr) ? cleanStr : null;
    },
    /**
     * @param {string} str
     * @returns {object}
     */
    json: str => {
      try {
        return JSON.parse(str);
      } catch {
        return null;
      }
    },
    /**
     * @param {string} str
     * @returns {string[]}
     */
    stringArray: str => {
      return !str ? null : str.split(',');
    },
    /**
     * @param {string} str
     * @returns {number}
     */
    float: str => {
      if (isNil(str) || !str.length) {
        return null;
      } else {
        const result = parseFloat(str);
        return isNaN(result) ? null : result;
      }
    },
  };

  static Encoders = {
    /**
     * @param {number} value
     * @returns {string}
     */
    amount: value => centsAmountToFloat(value)?.toString() || null,
    /**
     * @param {boolean} value
     * @returns {string}
     */
    boolean: value => (value ? 'true' : 'false'),
    /**
     * @param {number} value
     * @returns {number}
     */
    integer: value => value.toString(),
    /**
     * @param {object} str
     * @returns {string}
     */
    json: value => JSON.stringify(value),
    /**
     * @param {string[]} str
     * @returns {string}
     */
    stringArray: value => value.join(','),
    /**
     * @param {number} value
     * @returns {number}
     */
    float: value => value.toString(),
  };

  /**
   * Decode a query object as provided by router.query according to `config`
   * @param {object} queryObject
   */
  decode(queryObject) {
    const result = {};
    Object.entries(this.config).forEach(([key, param]) => {
      if (!has(queryObject, key)) {
        return;
      }

      // Decode value
      const value = queryObject[key];
      const isAlias = param.type === 'alias';
      const targetKey = isAlias ? param.on : key;
      const fieldType = this.config[targetKey].type;
      const decoder = UrlQueryHelper.Decoders[fieldType];
      const decodedValue = decoder ? decoder(value) : value;
      result[targetKey] = param.modifier ? param.modifier(decodedValue) : decodedValue;
    });

    return result;
  }

  /**
   * Encode the values of `queryObject` to make it safe to pass in router.push
   * @param {object} queryObject
   */
  encode(queryObject) {
    const result = {};
    Object.entries(queryObject).forEach(([key, value]) => {
      const param = this.config[key];
      if (!param) {
        return;
      }

      // Encode value
      const isAlias = param.type === 'alias';
      const targetKey = isAlias ? param.on : key;
      const fieldType = this.config[targetKey].type;
      const encoder = UrlQueryHelper.Encoders[fieldType];
      const encodedValue = encoder ? encoder(value) : value;

      // If it's an aliased field, set the value on the referenced field
      result[targetKey] = encodedValue;
    });

    return result;
  }
}
